diff --git a/catroid/src/org/catrobat/catroid/common/Constants.java b/catroid/src/org/catrobat/catroid/common/Constants.java index 012b19fea8c..efc95fce222 100644 --- a/catroid/src/org/catrobat/catroid/common/Constants.java +++ b/catroid/src/org/catrobat/catroid/common/Constants.java @@ -2,21 +2,21 @@ * Catroid: An on-device visual programming system for Android devices * Copyright (C) 2010-2013 The Catrobat Team * () - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * An additional term exception under section 7 of the GNU Affero * General Public License, version 3, is available at * http://developer.catrobat.org/license_additional_term - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ @@ -34,6 +34,7 @@ public final class Constants { public static final int APPLICATION_BUILD_NUMBER = 0; // updated from jenkins nightly/release build public static final String APPLICATION_BUILD_NAME = ""; // updated from jenkins nightly/release build public static final String PROJECTCODE_NAME = "code.xml"; + public static final String PROJECTCODE_NAME_TMP = "tmp_" + PROJECTCODE_NAME; public static final String CATROBAT_EXTENSION = ".catrobat"; public static final String IMAGE_STANDARD_EXTENTION = ".png"; @@ -84,11 +85,10 @@ public final class Constants { public static final String EXTRA_X_VALUE_POCKET_PAINT = "org.catrobat.extra.PAINTROID_X"; public static final String EXTRA_Y_VALUE_POCKET_PAINT = "org.catrobat.extra.PAINTROID_Y"; public static final String POCKET_PAINT_PACKAGE_NAME = "org.catrobat.paintroid"; + public static final String POCKET_PAINT_DOWNLOAD_LINK = "market://details?id=" + POCKET_PAINT_PACKAGE_NAME; public static final String POCKET_PAINT_INTENT_ACTIVITY_NAME = "org.catrobat.paintroid.MainActivity"; - //Various: public static final int BUFFER_8K = 8 * 1024; - public static final String POCKET_PAINT_DOWNLOAD_LINK = "market://details?id=" + POCKET_PAINT_PACKAGE_NAME; public static final String PREF_PROJECTNAME_KEY = "projectName"; public static final String PROJECTNAME_TO_LOAD = "projectNameToLoad"; diff --git a/catroid/src/org/catrobat/catroid/io/StorageHandler.java b/catroid/src/org/catrobat/catroid/io/StorageHandler.java index ac3f98fcfea..40ce6c0f4a1 100644 --- a/catroid/src/org/catrobat/catroid/io/StorageHandler.java +++ b/catroid/src/org/catrobat/catroid/io/StorageHandler.java @@ -26,6 +26,8 @@ import android.graphics.Bitmap.CompressFormat; import android.util.Log; +import com.google.common.base.Charsets; +import com.google.common.io.Files; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.reflection.FieldDictionary; import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider; @@ -130,6 +132,7 @@ import static org.catrobat.catroid.common.Constants.IMAGE_DIRECTORY; import static org.catrobat.catroid.common.Constants.NO_MEDIA_FILE; import static org.catrobat.catroid.common.Constants.PROJECTCODE_NAME; +import static org.catrobat.catroid.common.Constants.PROJECTCODE_NAME_TMP; import static org.catrobat.catroid.common.Constants.SOUND_DIRECTORY; import static org.catrobat.catroid.utils.Utils.buildPath; import static org.catrobat.catroid.utils.Utils.buildProjectPath; @@ -279,6 +282,10 @@ public File getBackPackSoundDirectory() { } public Project loadProject(String projectName) { + codeFileSanityCheck(projectName); + + Log.d(TAG, "loadProject " + projectName); + loadSaveLock.lock(); try { File projectCodeFile = new File(buildProjectPath(projectName), PROJECTCODE_NAME); @@ -311,22 +318,51 @@ public boolean cancelLoadProject() { return false; } + public boolean saveProject(Project project) { BufferedWriter writer = null; + if (project == null) { + return false; + } + + Log.d(TAG, "saveProject " + project.getName()); + + codeFileSanityCheck(project.getName()); + loadSaveLock.lock(); + + String projectXml; + File tmpCodeFile = null; + File currentCodeFile = null; + try { - if (project == null) { - return false; + projectXml = XML_HEADER.concat(xstream.toXML(project)); + tmpCodeFile = new File(buildProjectPath(project.getName()), PROJECTCODE_NAME_TMP); + currentCodeFile = new File(buildProjectPath(project.getName()), PROJECTCODE_NAME); + + if (currentCodeFile.exists()) { + try { + String oldProjectXml = Files.toString(currentCodeFile, Charsets.UTF_8); + + if (oldProjectXml.equals(projectXml)) { + Log.d(TAG, "Project version is the same. Do not update " + currentCodeFile.getName()); + return false; + } + Log.d(TAG, "Project version differ <" + oldProjectXml.length() + "> <" + + projectXml.length() + ">. update " + currentCodeFile.getName()); + + } catch (Exception exception) { + Log.e(TAG, "Opening old project " + currentCodeFile.getName() + " failed.", exception); + return false; + } } File projectDirectory = new File(buildProjectPath(project.getName())); createProjectDataStructure(projectDirectory); - writer = new BufferedWriter(new FileWriter(new File(projectDirectory, PROJECTCODE_NAME)), - Constants.BUFFER_8K); - String projectXml = XML_HEADER.concat(xstream.toXML(project)); + writer = new BufferedWriter(new FileWriter(tmpCodeFile), Constants.BUFFER_8K); writer.write(projectXml); writer.flush(); return true; @@ -337,10 +373,53 @@ public boolean saveProject(Project project) { if (writer != null) { try { writer.close(); + + if (currentCodeFile.exists() && !currentCodeFile.delete()) { + Log.e(TAG, "Could not delete " + currentCodeFile.getName()); + } + + if (!tmpCodeFile.renameTo(currentCodeFile)) { + Log.e(TAG, "Could not rename " + currentCodeFile.getName()); + } + } catch (IOException ioException) { Log.e(TAG, "Failed closing the buffered writer", ioException); } } + + loadSaveLock.unlock(); + } + } + + public void codeFileSanityCheck(String projectName) { + loadSaveLock.lock(); + + try { + File tmpCodeFile = new File(buildProjectPath(projectName), PROJECTCODE_NAME_TMP); + + if (tmpCodeFile.exists()) { + File currentCodeFile = new File(buildProjectPath(projectName), PROJECTCODE_NAME); + if (currentCodeFile.exists()) { + Log.w(TAG, "TMP File probably corrupted. Both files exist. Discard " + tmpCodeFile.getName()); + + if (!tmpCodeFile.delete()) { + Log.e(TAG, "Could not delete " + tmpCodeFile.getName()); + } + + return; + } + + Log.w(TAG, "Process interrupted before renaming. Rename " + PROJECTCODE_NAME_TMP + + " to " + PROJECTCODE_NAME); + + if (!tmpCodeFile.renameTo(currentCodeFile)) { + Log.e(TAG, "Could not rename " + tmpCodeFile.getName()); + } + + } + } catch (Exception exception) { + Log.e(TAG, "Exception " + exception); + } finally { loadSaveLock.unlock(); } } @@ -432,15 +511,13 @@ public File copySoundFile(String path) throws IOException, IllegalArgumentExcept File outputFile = new File(buildPath(soundDirectory.getAbsolutePath(), inputFileChecksum + "_" + inputFile.getName())); - return copyFileAddCheckSum(outputFile, inputFile, soundDirectory); + return copyFileAddCheckSum(outputFile, inputFile); } public File copySoundFileBackPack(SoundInfo selectedSoundInfo) throws IOException, IllegalArgumentException { String path = selectedSoundInfo.getAbsolutePath(); - File backPackDirectory = new File(buildPath(DEFAULT_ROOT, BACKPACK_DIRECTORY, BACKPACK_SOUND_DIRECTORY)); - File inputFile = new File(path); if (!inputFile.exists() || !inputFile.canRead()) { throw new IllegalArgumentException("file " + path + " doesn`t exist or can`t be read"); @@ -452,7 +529,7 @@ public File copySoundFileBackPack(SoundInfo selectedSoundInfo) throws IOExceptio File outputFile = new File(buildPath(DEFAULT_ROOT, BACKPACK_DIRECTORY, BACKPACK_SOUND_DIRECTORY, currentProject + "_" + selectedSoundInfo.getTitle() + "_" + inputFileChecksum)); - return copyFileAddCheckSum(outputFile, inputFile, backPackDirectory); + return copyFileAddCheckSum(outputFile, inputFile); } public File copyImage(String currentProjectName, String inputFilePath, String newName) throws IOException { @@ -493,7 +570,7 @@ public File copyImage(String currentProjectName, String inputFilePath, String ne } File outputFile = new File(newFilePath); - return copyFileAddCheckSum(outputFile, inputFile, imageDirectory); + return copyFileAddCheckSum(outputFile, inputFile); } } @@ -512,7 +589,7 @@ public File makeTempImageCopy(String inputFilePath) throws IOException { File outputFile = new File(Constants.TMP_IMAGE_PATH); - File copiedFile = UtilFile.copyFile(outputFile, inputFile, tempDirectory); + File copiedFile = UtilFile.copyFile(outputFile, inputFile); return copiedFile; } @@ -586,11 +663,18 @@ public void fillChecksumContainer() { } public String getXMLStringOfAProject(Project project) { - return xstream.toXML(project); + loadSaveLock.lock(); + String xmlProject = ""; + try { + xmlProject = xstream.toXML(project); + } finally { + loadSaveLock.unlock(); + } + return xmlProject; } - private File copyFileAddCheckSum(File destinationFile, File sourceFile, File directory) throws IOException { - File copiedFile = UtilFile.copyFile(destinationFile, sourceFile, directory); + private File copyFileAddCheckSum(File destinationFile, File sourceFile) throws IOException { + File copiedFile = UtilFile.copyFile(destinationFile, sourceFile); addChecksum(destinationFile, sourceFile); return copiedFile; diff --git a/catroid/src/org/catrobat/catroid/utils/CopyProjectTask.java b/catroid/src/org/catrobat/catroid/utils/CopyProjectTask.java index 00ba077e86c..b4744f58f0d 100644 --- a/catroid/src/org/catrobat/catroid/utils/CopyProjectTask.java +++ b/catroid/src/org/catrobat/catroid/utils/CopyProjectTask.java @@ -103,7 +103,7 @@ private void copyDirectory(File destinationFile, File sourceFile) throws IOExcep copyDirectory(new File(destinationFile, subDirectoryName), new File(sourceFile, subDirectoryName)); } } else { - UtilFile.copyFile(destinationFile, sourceFile, null); + UtilFile.copyFile(destinationFile, sourceFile); } } } diff --git a/catroid/src/org/catrobat/catroid/utils/UtilFile.java b/catroid/src/org/catrobat/catroid/utils/UtilFile.java index 8d4b23a883a..eea15ecc04b 100644 --- a/catroid/src/org/catrobat/catroid/utils/UtilFile.java +++ b/catroid/src/org/catrobat/catroid/utils/UtilFile.java @@ -2,21 +2,21 @@ * Catroid: An on-device visual programming system for Android devices * Copyright (C) 2010-2013 The Catrobat Team * () - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * An additional term exception under section 7 of the GNU Affero * General Public License, version 3, is available at * http://developer.catrobat.org/license_additional_term - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ @@ -46,10 +46,6 @@ import java.util.Locale; public final class UtilFile { - public enum FileType { - TYPE_IMAGE_FILE, TYPE_SOUND_FILE - } - private static final String TAG = UtilFile.class.getSimpleName(); // Suppress default constructor for noninstantiability @@ -209,7 +205,7 @@ public boolean accept(File dir, String filename) { return projectList; } - public static File copyFile(File destinationFile, File sourceFile, File directory) throws IOException { + public static File copyFile(File destinationFile, File sourceFile) throws IOException { FileInputStream inputStream = null; FileChannel inputChannel = null; FileOutputStream outputStream = null; @@ -342,4 +338,7 @@ public static String decodeSpecialCharsForFileSystem(String projectName) { return projectName; } + public enum FileType { + TYPE_IMAGE_FILE, TYPE_SOUND_FILE + } } diff --git a/catroidTest/src/org/catrobat/catroid/test/io/StorageHandlerTest.java b/catroidTest/src/org/catrobat/catroid/test/io/StorageHandlerTest.java index d1f16df55d1..9dabc80665c 100644 --- a/catroidTest/src/org/catrobat/catroid/test/io/StorageHandlerTest.java +++ b/catroidTest/src/org/catrobat/catroid/test/io/StorageHandlerTest.java @@ -2,21 +2,21 @@ * Catroid: An on-device visual programming system for Android devices * Copyright (C) 2010-2013 The Catrobat Team * () - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * An additional term exception under section 7 of the GNU Affero * General Public License, version 3, is available at * http://developer.catrobat.org/license_additional_term - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ @@ -24,6 +24,9 @@ import android.test.AndroidTestCase; +import com.google.common.base.Charsets; +import com.google.common.io.Files; + import org.catrobat.catroid.ProjectManager; import org.catrobat.catroid.common.LookData; import org.catrobat.catroid.common.StandardProjectHandler; @@ -40,11 +43,16 @@ import org.catrobat.catroid.io.StorageHandler; import org.catrobat.catroid.test.utils.Reflection; import org.catrobat.catroid.test.utils.TestUtils; +import org.catrobat.catroid.utils.UtilFile; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import static org.catrobat.catroid.common.Constants.PROJECTCODE_NAME; +import static org.catrobat.catroid.common.Constants.PROJECTCODE_NAME_TMP; +import static org.catrobat.catroid.utils.Utils.buildProjectPath; + public class StorageHandlerTest extends AndroidTestCase { private final StorageHandler storageHandler; private final String projectName = TestUtils.DEFAULT_TEST_PROJECT_NAME; @@ -194,7 +202,76 @@ public void testDefaultProject() throws IOException { } } - // TODO: add XML header validation based on xsd + public void testSanityCheck() throws IOException { + final int xPosition = 457; + final int yPosition = 598; + final float size = 0.8f; + + final Project project = new Project(getContext(), projectName); + Sprite firstSprite = new Sprite("first"); + Sprite secondSprite = new Sprite("second"); + Sprite thirdSprite = new Sprite("third"); + Sprite fourthSprite = new Sprite("fourth"); + Script testScript = new StartScript(firstSprite); + Script otherScript = new StartScript(secondSprite); + HideBrick hideBrick = new HideBrick(firstSprite); + ShowBrick showBrick = new ShowBrick(firstSprite); + SetSizeToBrick setSizeToBrick = new SetSizeToBrick(secondSprite, size); + ComeToFrontBrick comeToFrontBrick = new ComeToFrontBrick(firstSprite); + PlaceAtBrick placeAtBrick = new PlaceAtBrick(secondSprite, xPosition, yPosition); + + testScript.addBrick(hideBrick); + testScript.addBrick(showBrick); + testScript.addBrick(setSizeToBrick); + testScript.addBrick(comeToFrontBrick); + + otherScript.addBrick(placeAtBrick); + otherScript.setPaused(true); + + firstSprite.addScript(testScript); + secondSprite.addScript(otherScript); + + project.addSprite(firstSprite); + project.addSprite(secondSprite); + project.addSprite(thirdSprite); + project.addSprite(fourthSprite); + + + File tmpCodeFile = new File(buildProjectPath(project.getName()), PROJECTCODE_NAME_TMP); + File currentCodeFile = new File(buildProjectPath(project.getName()), PROJECTCODE_NAME); + assertFalse(tmpCodeFile.getName() + " exists!", tmpCodeFile.exists()); + assertFalse(currentCodeFile.getName() + " exists!", currentCodeFile.exists()); + + storageHandler.saveProject(project); + + assertTrue(currentCodeFile.getName() + " was not created!", currentCodeFile.exists()); + assertTrue(PROJECTCODE_NAME + " is empty!", currentCodeFile.length() > 0); + + // simulate 1st Option: tmp_code.xml exists but code.xml doesn't exist --> saveProject process will restore from tmp_code.xml + if (!tmpCodeFile.createNewFile()) { + fail("Could not create tmp file"); + } + UtilFile.copyFile(tmpCodeFile, currentCodeFile); + String currentCodeFileXml = Files.toString(currentCodeFile, Charsets.UTF_8); + assertTrue("Could not delete " + currentCodeFile.getName(), currentCodeFile.delete()); + + storageHandler.saveProject(project); + + assertTrue(currentCodeFile.getName() + " was not created!", currentCodeFile.exists()); + assertTrue(PROJECTCODE_NAME + " is empty!", currentCodeFile.length() > 0); + assertTrue("Sanity Check Failed. New Code File is not equal with tmp file.", currentCodeFileXml.equals(Files.toString(currentCodeFile, Charsets.UTF_8))); + + // simulate 2nd Option: tmp_code.xml and code.xml exist --> saveProject process will discard tmp_code.xml and use code.xml + if (!tmpCodeFile.createNewFile()) { + fail("Could not create tmp file"); + } + + storageHandler.saveProject(project); + + assertFalse("Sanity Check Failed. tmp file was not discarded.", tmpCodeFile.exists()); + } + + // TODO: add XML header validation based on xsd // public void testAliasesAndXmlHeader() { // // String projectName = "myProject"; diff --git a/catroidTest/src/org/catrobat/catroid/test/utils/TestUtils.java b/catroidTest/src/org/catrobat/catroid/test/utils/TestUtils.java index ed3b1be01dc..3b28ceb8758 100644 --- a/catroidTest/src/org/catrobat/catroid/test/utils/TestUtils.java +++ b/catroidTest/src/org/catrobat/catroid/test/utils/TestUtils.java @@ -2,21 +2,21 @@ * Catroid: An on-device visual programming system for Android devices * Copyright (C) 2010-2013 The Catrobat Team * () - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * An additional term exception under section 7 of the GNU Affero * General Public License, version 3, is available at * http://developer.catrobat.org/license_additional_term - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ @@ -73,16 +73,12 @@ private TestUtils() { /** * saves a file into the project folder * if project == null or "" file will be saved into Catroid folder - * - * @param project - * Folder where the file will be saved, this folder should exist - * @param name - * Name of the file - * @param fileID - * the id of the file --> needs the right context + * + * @param project Folder where the file will be saved, this folder should exist + * @param name Name of the file + * @param fileID the id of the file --> needs the right context * @param context - * @param type - * type of the file: 0 = imagefile, 1 = soundfile + * @param type type of the file: 0 = imagefile, 1 = soundfile * @return the file * @throws IOException */ diff --git a/catroidTest/src/org/catrobat/catroid/test/utiltests/UtilsTest.java b/catroidTest/src/org/catrobat/catroid/test/utiltests/UtilsTest.java index 0b52e06129f..ab926acf601 100644 --- a/catroidTest/src/org/catrobat/catroid/test/utiltests/UtilsTest.java +++ b/catroidTest/src/org/catrobat/catroid/test/utiltests/UtilsTest.java @@ -22,6 +22,7 @@ */ package org.catrobat.catroid.test.utiltests; +import android.os.SystemClock; import android.test.AndroidTestCase; import org.catrobat.catroid.common.Constants; @@ -64,6 +65,7 @@ public class UtilsTest extends AndroidTestCase { @Override protected void setUp() throws Exception { OutputStream outputStream = null; + TestUtils.deleteTestProjects(NEW_PROGRAM_NAME); try { testFile = File.createTempFile("testCopyFiles", ".txt"); if (testFile.canWrite()) { @@ -90,6 +92,7 @@ protected void tearDown() throws Exception { if (copiedFile != null && copiedFile.exists()) { copiedFile.delete(); } + TestUtils.deleteTestProjects(NEW_PROGRAM_NAME); super.tearDown(); } @@ -195,6 +198,7 @@ public void testProjectSameAsStandardProject() { removeScriptAndCompareToStandardProject(); removeSpriteAndCompareToStandardProject(); + SystemClock.sleep(1000); } private void addSpriteAndCompareToStandardProject() { diff --git a/catroidTest/src/org/catrobat/catroid/uitest/content/brick/SpeakBrickTest.java b/catroidTest/src/org/catrobat/catroid/uitest/content/brick/SpeakBrickTest.java index 8438aacfd4f..a6eede48dd5 100644 --- a/catroidTest/src/org/catrobat/catroid/uitest/content/brick/SpeakBrickTest.java +++ b/catroidTest/src/org/catrobat/catroid/uitest/content/brick/SpeakBrickTest.java @@ -88,6 +88,7 @@ public void testSpeakBrick() { String brickText = (String) Reflection.getPrivateField(speakBrick, "text"); assertEquals("Wrong text in field.", testString, brickText); + solo.waitForText(testString); assertEquals("Value in Brick is not updated.", testString, ((TextView) solo.getView(R.id.brick_speak_edit_text)).getText().toString()); @@ -98,6 +99,7 @@ public void testSpeakBrick() { brickText = (String) Reflection.getPrivateField(speakBrick, "text"); assertEquals("Wrong text in field.", leading, brickText); + solo.waitForText(leading); assertEquals("Value in Brick is not updated.", leading, ((TextView) solo.getView(R.id.brick_speak_edit_text)) .getText().toString()); @@ -108,6 +110,7 @@ public void testSpeakBrick() { brickText = (String) Reflection.getPrivateField(speakBrick, "text"); assertEquals("Wrong text in field.", trailing, brickText); + solo.waitForText(trailing); assertEquals("Value in Brick is not updated.", trailing, ((TextView) solo.getView(R.id.brick_speak_edit_text)) .getText().toString()); } diff --git a/catroidTest/src/org/catrobat/catroid/uitest/ui/fragment/LookFragmentTest.java b/catroidTest/src/org/catrobat/catroid/uitest/ui/fragment/LookFragmentTest.java index d5344c6905c..05cdee32e36 100644 --- a/catroidTest/src/org/catrobat/catroid/uitest/ui/fragment/LookFragmentTest.java +++ b/catroidTest/src/org/catrobat/catroid/uitest/ui/fragment/LookFragmentTest.java @@ -260,6 +260,7 @@ public void testDeleteLookContextMenu() { assertEquals("New count is not correct - one look should be deleted", 1, newCount); assertEquals("Count of the lookDataList is not correct", newCount, lookDataList.size()); + Log.d("LookFragmentTest", "path: " + lookToDelete.getAbsolutePath()); File deletedFile = new File(lookToDelete.getAbsolutePath()); assertFalse("File should be deleted", deletedFile.exists()); }